#coding:utf-8

import sys
import types

import dataparser
import utils
import error
from error import Failure, Warning, HasNotThisArgument

__TEST__ = True


NUMBER = 0
STRING = 1
ARRAY = 2

class Argument(dict):
    def __init__(self, initdata=None, format=None):
        if not isinstance(initdata, types.DictionaryType):
            raise ValueError("Only dict argument is allowed: %s"%initdata)
        self.update(initdata)
        self._format = format or {}
    
    def __getattr__(self, attr):
        if not self.has_key(attr):
            raise HasNotThisArgument("Has not this argument: %s"%attr)
        value = self[attr]
        format = self._format.get(attr, None)
        if format == NUMBER:
            return self._convert_to_number(value)
        elif format == STRING:
            return self._convert_to_string(value)
        elif format == ARRAY:
            return self._convert_to_array(value)
        else:
            return value
        
    def _convert_to_number(self, value):
        try:
            return int(value)
        except:
            try:
                return float(value)
            except:
                return value
            
    def _convert_to_string(self, value):
        if isinstance(value, types.StringTypes):
            return unicode(value, encoding='utf-8')
        return unicode(value)
        
    def _convert_to_array(self, value):
        try:
            return eval(value)
        except:
            return value
    
    def _update_argument(self, key, value):
        self[key] = value
        

class TestCase(object):
    
    def __init__(self, func, format=None):
        self._func = func
        self._func_setup_method = self.globals.get('setup')
        self._func_cleanup_method = self.globals.get('cleanup')
        self._format = format
        self.description = (func.func_doc or '').strip()
        self.name = func.func_name
        self.module = func.__module__
        self.path = '%s.%s'%(self.module, self.name)
        arg_count = func.func_code.co_argcount
        if arg_count != 1:
            raise ValueError("Testcase %s should have only 1 arg but have %d"%(self.path, arg_count))
        self.filepath = func.func_code.co_filename
        self.arguments = []
        
    @property
    def globals(self):
        return self._func.func_globals
        
    def __str__(self):
        return self.path
    
    def __repr__(self):
        return "<TestCase %s with %d data args>"%(self.path, len(self.arguments))
    
    def __cmp__(self, other):
        return cmp(str(self), str(other))
    
    def __eq__(self, other):
        if type(self) is not type(other):
            return False
        return self._func == other._func
    
    def __call__(self, args=None, result=None):
        if result:
            if args:
                self.add_argument(args)
            return self.run(result)
        else:
            args = args or {}
            return self.debug(**args)
    
    def add_argument(self, args):
        if isinstance(args, (types.ListType, types.TupleType)):
            for arg in args:
                self.arguments.append(Argument(arg, self._format))
        else:
            self.arguments.append(Argument(args, self._format))
    
    def _run(self, args):
        """ if setup failed, cleanup can not be execed;if test failed,
        cleanup can be execed.
        return error info list, if result is successed, it is empty list.
        
        @args: argument sets the function need
        """
        
        try:
            if callable(self._func_setup_method):
                args._update_argument('SETUP_RESULT', self._func_setup_method())
        except:
            return [sys.exc_info()]
        
        error_list = []
        try:
            self._func(args)
        except:
            error_list.append(sys.exc_info())
        try:
            if callable(self._func_cleanup_method):
                self._func_cleanup_method()
        except:
            error_list.append(sys.exc_info())
            
        if hasattr(self, 'warnings'):
            warning_list = getattr(self, 'warnings', None)
            if isinstance(warning_list, types.ListType):
                error_list += warning_list
            delattr(self, 'warnings')

        return error_list
    
    def run(self, result):
        result.start_test(self)
        arguments = self.arguments
        if not arguments: # run test once at least 
            arguments = [{},] 
        for args in arguments:
            time_start = utils.now()
            self.setup()
            err_list = self._run(args)
            self.cleanup()
            time_stop = utils.now()
            result.add_test_result(self, args, err_list, time_start, time_stop)
        result.stop_test(self)
            
    def debug(self, print_msg=True, **kw):
        self.add_argument(kw)
        self.setup()
        err_list = self._run(self.arguments[0])
        self.cleanup()
        if print_msg:
            for etype, value, tb in err_list:
                sys.stderr.write('Error happened:\n')
                for msg, env in error.format_exception(etype, value, tb):
                    sys.stderr.write('  '+ msg + '\n')
        return err_list
    
    def setup(self):
        """ override if your subclass want to implement works before test"""
        pass
    
    def cleanup(self):
        """ override if your subclass want to implement works after test"""
        pass

class TestSuite(object):
    """ Test Suite for collecting test cases.
    test suite could only include 'TestCase' instance.
        
    for 'make_suite' method:
    @dir_path is testcase directory path
    @suite_list is a list included many module path.
    the format of module path should be:
            
        '.'    # the main directory
        'package'    # one dir name in the main directory
        'package.subpackage'    # one sub dir
        'package.subpackage.script'    # one script name
        'package.subpackage.script.function' # one function
    """
    def __init__(self, tests = []):
        self._tests = []
        for test in tests:
            self.add_test(test)
    
    def __str__(self):
        return "TestSuite tests=%s"%self._tests
    
    __repr__ = __str__

    def __iter__(self):
        return iter(self._tests)
    
    def __len__(self):
        return len(self._tests)

    def add_test(self, test_case):
        """ If test case has existed in the test suite, then add
        its test arguments to one in the test suite, else add
        this test case to test suite.
        """
        if not isinstance(test_case, TestCase):
            raise TypeError("TestCase instance is needed")
        # FIXME: it is really ugly wrtting style
        for test in self._tests:
            if test_case.path == test.path:
                for args in test_case.arguments:
                    test.add_argument(args)
                break
        else:
            self._tests.append(test_case)
        
    def add_test_suite(self, test_suite):
        if not isinstance(test_suite, self.__class__):
            raise TypeError, "TestSuite instance is needed"
        for test in test_suite:
            self.add_test(test)
        
    def add_test_from_module(self, module):
        for obj in module.__dict__.itervalues():
            if isinstance(obj, TestCase):
                self.add_test(obj)
                
    def add_test_from_name(self, name, dir_path=None):
        if dir_path is not None:
            sys.path.insert(0, dir_path)
        name_s = name.rsplit('.', 1)
        obj = __import__(name_s[0], fromlist=['from %s import *'%name_s[0]])
        if len(name_s) == 2:
            if hasattr(obj, name_s[1]):
                obj = getattr(obj, name_s[1])
            else:
                obj = __import__(name, fromlist=['from %s import *'%name])
                
        if isinstance(obj, types.ModuleType):
            self.add_test_from_module(obj)
        else:
            self.add_test(obj)
        if dir_path is not None:
            sys.path.remove(dir_path)
    
    def make_suite(self, dir_path, suite_list):
        dir_path = utils.get_abs_path(dir_path)
        for suitename in suite_list:
            items = utils.find_all_scripts(dir_path, suitename)
            if isinstance(items, (types.ListType,  types.TupleType)): # suitename is not dir
                for item in items:
                    self.add_test_from_name(item, dir_path)
            else:
                self.add_test_from_name(items, dir_path)
        self.collect_test_data()
    
    def collect_test_data(self):
        for test in self._tests:
            fpaths = utils.find_same_name(test.filepath)
            data = dataparser.parse_data(fpaths).get(test.name)
            if data:
                test.add_argument(data)
    
    
class _warning_traceback:
    """ Simulates traceback object so that it looks like
    a exception.
    """
    def __init__(self, frame, next):
        self.tb_frame = frame
        self.tb_lineno = frame.f_lineno
        self.tb_lasti = frame.f_lasti
        self.tb_next = next
        
def _add_warning_to_test_case(warning_message, stacklimit=2):
    frame = sys._getframe()
    while frame and stacklimit > 0:
        frame = frame.f_back
        stacklimit -= 1
    tb = None
    while frame:
        tb = _warning_traceback(frame, tb)
        frame = frame.f_back
    etype = Warning
    value = Warning(warning_message)
    # find which test case, or not found
    while frame:
        tc = frame.f_locals.get('self', None)
        if isinstance(tc, TestCase):
            if not hasattr(tc, 'warnings'):
                tc.warnings = []
            tc.warnings.append((etype, value, tb))
            break
        frame = frame.f_back


class VerifyMethod:
    @staticmethod
    def warn(msg=None):
        _add_warning_to_test_case(msg)
    @staticmethod
    def warn_if(expr, msg=None):
        if expr:
            _add_warning_to_test_case(msg)
    @staticmethod
    def warn_if_not(expr, msg=None):
        if not expr:
            _add_warning_to_test_case(msg)
    @staticmethod
    def warn_equal(first, second, msg=None):
        if first == second:
            _add_warning_to_test_case(msg or "%s = %s"%(first, second))
    @staticmethod  
    def warn_not_equal(first, second, msg=None):
        if first != second:
            _add_warning_to_test_case(msg or "%s != %s"%(first, second))
    @staticmethod    
    def warn_equal_number(first, second, msg=None):
        try:
            first = float(first)
            second = float(second)
        except:
            pass
        if first == second:
            _add_warning_to_test_case(msg or "%s = %s"%(first, second))
    @staticmethod    
    def warn_not_equal_number(first, second, msg=None):
        try:
            first = float(first)
            second = float(second)
        except:
            pass
        if first != second:
            _add_warning_to_test_case(msg or "%s != %s"%(first, second))
    @staticmethod
    def fail(msg=None):
        raise Failure(msg)
    @staticmethod
    def fail_if(expr, msg=None):
        if expr:
            raise Failure(msg)
    @staticmethod
    def fail_if_not(expr, msg=None):
        if not expr:
            raise Failure(msg)
    @staticmethod
    def fail_equal(first, second, msg=None):
        if first == second:
            raise Failure(msg or "%s = %s"%(first, second))
    @staticmethod  
    def fail_not_equal(first, second, msg=None):
        if first != second:
            raise Failure(msg or "%s != %s"%(first, second))
    @staticmethod    
    def fail_equal_number(first, second, msg=None):
        try:
            first = float(first)
            second = float(second)
        except:
            pass
        if first == second:
            raise Failure(msg or "%s = %s"%(first, second))
    @staticmethod    
    def fail_not_equal_number(first, second, msg=None):
        try:
            first = float(first)
            second = float(second)
        except:
            pass
        if first != second:
            raise Failure(msg or "%s != %s"%(first, second))


class TestCaseDescriptor(object):
    
    def __call__(self, *funcs, **formats):
        if len(funcs) != 0:
            return TestCase(funcs[0], format=formats)
        def wrapper(func):
            return TestCase(func, format=formats)
        return wrapper
    
test = TestCaseDescriptor()

